package com.longtu.chat.ws;

import com.alibaba.fastjson.JSON;
import com.longtu.chat.vo.IsTypingMessage;
import com.longtu.chat.vo.MyMessage;
import com.longtu.chat.util.Constant;
import com.longtu.chat.util.RedisUtils;
import com.longtu.chat.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * @author wtl
 * @date 2022/4/15 14:40
 */
@Slf4j
@ServerEndpoint(value = "/test/one/{name}/{avatar}/{x}/{y}")
@Component
public class MessageWebSocket {
    public static RedisUtils redisUtils;
    public static Integer distanceMax = 3000;
    private static final String defaultGroupName = "聊天室";
    @Autowired
    public void setRedisOperator(RedisUtils redisUtils){
        MessageWebSocket.redisUtils = redisUtils;
    }

    /**
     * 记录当前在线连接数
     */
    private static final AtomicInteger onlineCount = new AtomicInteger(0);

    /**
     * 存放所有在线的客户端
     * key name
     * value session
     */
    private static final Map<String, Session> CLIENTS = new ConcurrentHashMap<>();

    /**
     * 存放所有用户信息
     * key name
     * value session
     */
    private static final Set<UserVo> USER_VO_LIST = new CopyOnWriteArraySet<>();

    /**
     * 存放所有用户的附近的人列表
     * key name
     * value session
     */
    private static final Map<String , Set<UserVo>> USER_FRIENDS_VO_ = new ConcurrentHashMap<>();

    /**
     * 存放聊天室集合
     * key 群聊名称
     * value 群聊客户端
     */
    private static final Map<String, Set<UserVo>> GROUPS = new ConcurrentHashMap<>();

    static {
        GROUPS.put(defaultGroupName, USER_VO_LIST);
        initDefaultGroupUser();
    }

    private static void initDefaultGroupUser() {
        UserVo userVo = new UserVo();
        userVo.setState(Constant.State.ONLINE.getState());
        userVo.setLoginName(defaultGroupName);
        userVo.setDistance("-1");
        USER_VO_LIST.add(userVo);
    }

    public static Set<?> getUsersName() {
        return CLIENTS.keySet();
    }

    public static Set<UserVo> getUserVoList() {
        return USER_VO_LIST;
    }

    public static UserVo getUserByName(String name) {
        for (UserVo userVo : USER_VO_LIST) {
            if (userVo.getLoginName().equals(name)) {
                return userVo;
            }
        }
        return null;
    }

    public static Session getSession(String name) {
        return CLIENTS.get(name);
    }

    // todo 定时检查session state  删除缓存 推送用户状态
    @Scheduled(fixedDelay = 5000)
    public void task(){
        for (Map.Entry<String, Session> entry : CLIENTS.entrySet()) {
            if (!entry.getValue().isOpen()) {
                CLIENTS.remove(entry.getKey());
                for (UserVo userVo : USER_VO_LIST) {
                    if (userVo.getLoginName().equals(entry.getKey())) {
                        USER_VO_LIST.remove(userVo);
                        break;
                    }
                }
            } else {
                pDisAndSend(entry.getKey());
            }
        }
    }


    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("name") String name,@PathParam("avatar") String avatar,
                       @PathParam("x") double x, @PathParam("y") double y) throws IOException {
        session.setMaxIdleTimeout(300000);
        // todo init时加入clients sendUserList时全部都要add
        if (CLIENTS.containsKey(name)) {
            // 一个客户端可能由于页面刷新等因素建立多个连接,这时服务端主动关闭 保留一份session即可
            CLIENTS.get(name).close();
        }
        CLIENTS.put(name, session);
        onlineCount.set(CLIENTS.size());
        // 保存用户信息
        redisUtils.geoAdd(name,x,y);
        log.info("有新连接加入：{}，当前在线为：{},登录地点为：东经：{}，北纬：{}", name, Arrays.toString(CLIENTS.keySet().toArray()), x, y);
        // todo 需要发送allUsers
        addUser(name, avatar);
        this.pDisAndSend(name);
    }

    private void addUser(String name, String avatar) {
        UserVo user = new UserVo();
        user.setLoginName(name);
        user.setAvatar(avatar);
        user.setState(Constant.State.ONLINE.getState());
        USER_VO_LIST.add(user);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        Iterator<Map.Entry<String, Session>> iterator = CLIENTS.entrySet().iterator();
        // 客户端主动关闭
        if (iterator.hasNext()) {
            Map.Entry<String, Session> sessionEntry = iterator.next();
            if (session.getId().equals(sessionEntry.getValue().getId())) {
                CLIENTS.remove(sessionEntry.getKey());
                // 通知所有客户端断开连接消息
                this.sendCloseMessage(sessionEntry.getKey());
                log.info("有一连接关闭：{}，当前在线人数为：{}", sessionEntry.getKey(), onlineCount.get());
            }
        }
        onlineCount.set(CLIENTS.size()); // 在线数更新
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param data 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String data, Session session) {
        log.info("服务端收到客户端[{}]的消息[{}]", session.getId(), data);
        try {
            MyMessage myMessage = JSON.parseObject(data, MyMessage.class);
            if (myMessage != null) {
                if (!myMessage.getIsGroup()) {
                    Session toSession = CLIENTS.get(myMessage.getSendTo());
                    chat_private(session, data, toSession);
                    return;
                }
                chat_group(data, myMessage);
            }
        } catch (Exception e) {
            // 直接转发
            IsTypingMessage typingMessage = JSON.parseObject(data, IsTypingMessage.class);
            String sendTo = typingMessage.getSendTo();
            if (sendTo == null || sendTo.equals(defaultGroupName)) {
                return;
            }
            Session toSession = getSession(sendTo);
            if (toSession != null) sendMessage(data, toSession);
            log.error("解析失败：{}", e.getMessage());
        }
    }

    private void chat_group(String data, MyMessage myMessage) {
        String groupName = myMessage.getSendTo();
        if (GROUPS.containsKey(groupName)) {
            // 发送群聊消息
            this.sendGroupMessage(data, myMessage.getSender());
        }
    }

    private void chat_private(Session session, String msg, Session toSession) {
        // 私聊时给别人发的同时也要给自己发，reason： 1。两个客户端都需要看到新消息 2.防止：服务器挂掉了，客户端自己虽然显示了新信息但是没发送成功
        if (toSession != null) this.sendMessage(msg, toSession);
        if (session != toSession) this.sendMessage(msg, session);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误: sessionId:{}", session.getId());
        error.printStackTrace();
    }

    /**
     * 服务端发送消息给客户端
     */
    private void sendMessage(String message, Session toSession) {
        try {
            String name = null;
            for (String value : CLIENTS.keySet()) {
                if (CLIENTS.get(value).getId().equals(toSession.getId())) {
                    name = value;
                    break;
                }
            }
            log.info("服务端给客户端[{}]发送消息[{}]", name, message);
            if (toSession.isOpen())toSession.getBasicRemote().sendText(message);
        } catch (Exception e) {
            log.error("服务端发送消息给客户端失败：{}", e.getMessage());
        }
    }

    /**
     * 服务端发送消息给客户端集合
     */
    private void sendGroupMessage(String message, String masterName) {
        try {
            Set<UserVo> userVos = USER_FRIENDS_VO_.get(masterName);
            for (UserVo userVo : userVos) {
                Session session = CLIENTS.get(userVo.getLoginName());
                if (session != null) {
                    sendMessage(message, session);
                }
            }
        } catch (Exception e) {
            log.error("服务端给其他所有客户端发消息失败:");
            e.printStackTrace();
        }
    }
    /**
     * 服务端发送消息给所有客户端
     */
    private void sendAllMessage(MyMessage myMessage){
        for (Map.Entry<String,Session> entry : CLIENTS.entrySet()){
            try {
                myMessage.setSendTo(entry.getKey());
                log.info("服务器给客户端：{}，发送：{}",entry.getKey(),JSON.toJSONString(myMessage));
                Session session = entry.getValue();
                if (session.isOpen())session.getBasicRemote().sendText(JSON.toJSONString(myMessage));
            } catch (IOException e) {
                log.info("解析失败:{}",e.getMessage());
            }
        }
    }

    public void pDisAndSend(String master){
        Set<UserVo> array = new HashSet<>();
        for (UserVo userVo : USER_VO_LIST) {
            if (userVo.getLoginName().equals(defaultGroupName)) {
                array.add(userVo);
                continue;
            }
            Integer value = getDistance(master, userVo);
            if (value > distanceMax) continue;
            userVo.setDistance(String.valueOf(value));
            array.add(userVo);
        }
        MyMessage resMyMessage = new MyMessage();
        resMyMessage.setMessageType(Constant.MessageType.Open.name());
        resMyMessage.setMessage(JSON.toJSONString(array));
        USER_FRIENDS_VO_.put(master, array);
        this.sendMessage(JSON.toJSONString(resMyMessage), getSession(master));
    }

    private int getDistance(String master, UserVo userVo) {
        Distance distance = redisUtils.geoCalculationDistance(master, userVo.getLoginName());
        if (distance == null) {
            return -1;
        }
        double distanceValue = distance.getValue() * 1000;
        return (int)(distanceValue /100) * 100;
    }

    /**
     * 通知所有客户端用户下线
     * @param name 下线用户名
     */
    private void sendCloseMessage(String name){
        MyMessage myMessage = new MyMessage();
        myMessage.setMessage(name);
        myMessage.setMessageType(Constant.MessageType.Close.name());
        this.sendAllMessage(myMessage);
    }
}


